﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.SharePoint;

namespace AgileShare.Framework.CamlQueries
{
	/// <summary>
	/// Builds a CAML query based on one or more Where and OrderBy clauses.	
	/// </summary>
	/// <example>This example shows one of the possible ways to use the CamlQueryBuilder.</example>
	/// <code>
	/// CamlQueryBuilder camlQueryBuilder = new CamlQueryBuilder();			
	/// 
	/// camlQueryBuilder.AddClause(LogicalJoin.First, "Field1", ComparisonOperator.BeginsWith, "Value1", ValueType.Text);
	/// camlQueryBuilder.AddClause(LogicalJoin.Or, Guid.NewGuid(), ComparisonOperator.Contains, "Value2", ValueType.ContentTypeId);
	/// camlQueryBuilder.AddClause(LogicalJoin.And, "Field3", ComparisonOperator.DateRangesOverlap, "Value3", ValueType.DateTime);
	/// camlQueryBuilder.AddClause(LogicalJoin.Or, Guid.NewGuid(), ComparisonOperator.Eq, "Value 4", ValueType.Text);
	/// 
	/// camlQueryBuilder.AddOrderBy(Guid.NewGuid(), OrderByDirection.Ascending);
	/// camlQueryBuilder.AddOrderBy("Field1", OrderByDirection.Descending);
	/// 
	/// camlQueryBuilder.AddGroupBy("Field1");
	/// camlQueryBuilder.AddGroupBy(Guid.NewGuid());
	/// 
	/// // Use of CamlQuery (which inherits from SPQuery), passing the CamlQueryBuilder in the constructor.
	/// // The query properties are automatically populated in the constructor using the CamlQueryBuilder object.
	/// CamlQuery query = new CamlQuery(camlQueryBuilder);
	/// SPList list = SPContext.Current.Web.GetList("MyList");
	/// list.GetItems(query);
	/// 
	/// // Use of SPQuery
	/// SPQuery query = new SPQuery();
	/// query.Query = camlQueryBuilder.SPQueryString;
	/// 
	/// // Get the complete CAML query as a string including &lt;Query&gt; element
	/// string camlQueryString = camlQueryBuilder.CamlQueryString;
	/// 
	/// // Get the complete CAML query XElement
	/// XElement queryElement = camlQueryBuilder.CamlQueryElement;	/// 
	/// </code>
	public class CamlQueryBuilder
	{
		/// <summary>
		/// The list of where clauses.
		/// </summary>
		public List<CamlQueryClause> QueryClauses;

		/// <summary>
		/// The list of order by clauses.
		/// </summary>
		public List<OrderByClause> OrderByClauses;

		/// <summary>
		/// The list of group by clauses.
		/// </summary>
		public List<GroupByClause> GroupByClauses;

		/// <summary>
		/// The list of view fields.
		/// </summary>
		public List<ViewField> ViewFields;

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		public CamlQueryBuilder()
		{
			this.QueryClauses = new List<CamlQueryClause>();
			this.OrderByClauses = new List<OrderByClause>();
			this.GroupByClauses = new List<GroupByClause>();
			this.ViewFields = new List<ViewField>();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="orderByFieldId">The order by field id.</param>
		/// <param name="orderByDirection">The order by direction.</param>
		/// <param name="viewFieldIds">The view field ids.</param>
		public CamlQueryBuilder(Guid orderByFieldId, OrderByDirection orderByDirection, params Guid[] viewFieldIds) : this()
		{
			this.AddOrderBy(orderByFieldId, orderByDirection);
			this.AddViewFields(viewFieldIds);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="orderByFieldName">Name of the order by field.</param>
		/// <param name="orderByDirection">The order by direction.</param>
		/// <param name="viewFieldNames">The view field names.</param>
		public CamlQueryBuilder(string orderByFieldName, OrderByDirection orderByDirection, params string[] viewFieldNames)
			: this()
		{
			this.AddOrderBy(orderByFieldName, orderByDirection);
			this.AddViewFields(viewFieldNames);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="fieldId">The field id.</param>
		/// <param name="comparisonOperator">The comparison operator.</param>
		/// <param name="value">The value.</param>
		/// <param name="valueType">Type of the value.</param>
		/// <param name="viewFieldIds">The view field ids.</param>
		public CamlQueryBuilder(Guid fieldId, ComparisonOperator comparisonOperator, object value, CamlValueType valueType, params Guid[] viewFieldIds)
			: this()
		{
			this.AddClause(LogicalJoin.First, fieldId, comparisonOperator, value, valueType);
			this.AddViewFields(viewFieldIds);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="fieldId">The field id.</param>
		/// <param name="comparisonOperator">The comparison operator.</param>
		/// <param name="value">The value.</param>
		/// <param name="valueType">Type of the value.</param>
		/// <param name="orderByFieldId">The order by field id.</param>
		/// <param name="orderByDirection">The order by direction.</param>
		/// <param name="viewFieldIds">The view field ids.</param>
		public CamlQueryBuilder(Guid fieldId, ComparisonOperator comparisonOperator, object value, CamlValueType valueType, Guid orderByFieldId, OrderByDirection orderByDirection, params Guid[] viewFieldIds)
			: this(fieldId, comparisonOperator, value, valueType, viewFieldIds)
		{
			this.AddOrderBy(orderByFieldId, orderByDirection);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="fieldName">Name of the field.</param>
		/// <param name="comparisonOperator">The comparison operator.</param>
		/// <param name="value">The value.</param>
		/// <param name="valueType">Type of the value.</param>
		/// <param name="viewFieldNames">The view field names.</param>
		public CamlQueryBuilder(string fieldName, ComparisonOperator comparisonOperator, object value, CamlValueType valueType, params string[] viewFieldNames)
			: this()
		{
			this.AddClause(LogicalJoin.First, fieldName, comparisonOperator, value, valueType);
			this.AddViewFields(viewFieldNames);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CamlQueryBuilder"/> class.
		/// </summary>
		/// <param name="fieldName">Name of the field.</param>
		/// <param name="comparisonOperator">The comparison operator.</param>
		/// <param name="value">The value.</param>
		/// <param name="valueType">Type of the value.</param>
		/// <param name="orderByFieldName">Name of the order by field.</param>
		/// <param name="orderByDirection">The order by direction.</param>
		/// <param name="viewFieldNames">The view field names.</param>
		public CamlQueryBuilder(string fieldName, ComparisonOperator comparisonOperator, object value, CamlValueType valueType, string orderByFieldName, OrderByDirection orderByDirection, params string[] viewFieldNames)
			: this(fieldName, comparisonOperator, value, valueType, viewFieldNames)
		{			
			this.AddOrderBy(orderByFieldName, orderByDirection);
		}

		/// <summary>
		/// Gets the CAML query as a string, including the top-level &lt;Query&gt; element.
		/// </summary>
		public string CamlQueryString
		{
			get
			{
				return this.ToString();
			}
		}

		/// <summary>
		/// Gets the CAML query as a string, including the top-level &lt;Query&gt; element.
		/// </summary>
		public string CamlQueryStringCondensed
		{
			get
			{
				XElement queryElement = BuildQueryElement();
				return queryElement.ToString(SaveOptions.DisableFormatting);
			}
		}

		/// <summary>
		/// Gets the CAML query formatted to be used in an SPQuery.Query, not including the top-level &lt;Query&gt; element and preserving white-space and format.
		/// </summary>
		public string SPQueryString
		{
			get
			{
				return BuildSPQueryString(false);
			}
		}

		/// <summary>
		/// Gets the CAML query formatted to be used in an SPQuery.Query, not including the top-level &lt;Query&gt; element and removing all extra-whitespace and format.
		/// </summary>
		public string SPQueryStringCondensed
		{
			get
			{
				return BuildSPQueryString(true);
			}
		}

		/// <summary>
		/// Gets the ViewFields element as a string.
		/// </summary>
		public string SPQueryViewFields
		{
			get
			{
				string viewFields = BuildSPQueryViewFields(false);
				return viewFields;
			}
		}

		/// <summary>
		/// Gets the ViewFields element as a string removing all extra-whitespace and format.
		/// </summary>
		public string SPQueryViewFieldsCondensed
		{
			get
			{
				string viewFields = BuildSPQueryViewFields(true);
				return viewFields;
			}
		}

		/// <summary>
		/// Gets the CAML query as an XElement, including the top-level &lt;Query&gt; element.
		/// </summary>
		public XElement CamlQueryElement
		{
			get
			{
				return this.BuildQueryElement();
			}
		}

		/// <summary>
		/// Gets the ViewFields as an XElement.
		/// </summary>
		public XElement ViewFieldsElement
		{
			get
			{
				return this.BuildViewFieldsElement();
			}
		}

		/// <summary>
		/// Adds a clause to the &lt;Where&gt; element.
		/// </summary>
		/// <param name="logicalJoin">The logical join (And, Or) that will connect the preceeding and the current element.</param>
		/// <param name="fieldId">The Field's ID as a Guid.</param>
		/// <param name="comparisonOperator">The comparison operator that will be applied.</param>
		/// <param name="value">The value that will be compared to.</param>
		/// <param name="valueType">The type of the value that will be compared.</param>
		public void AddClause(LogicalJoin logicalJoin, Guid fieldId, ComparisonOperator comparisonOperator, object value, CamlValueType valueType)
		{
			this.AddClause(logicalJoin, comparisonOperator, fieldId, null, valueType, value);
		}

		/// <summary>
		/// Adds a clause to the &lt;Where&gt; element.
		/// </summary>
		/// <param name="logicalJoin">The logical join (And, Or) that will connect the preceeding and the current element.</param>
		/// <param name="fieldRefName">The Field's internal name.</param>
		/// <param name="comparisonOperator">The comparison operator that will be applied.</param>
		/// <param name="value">The value that will be compared to.</param>
		/// <param name="valueType">The type of the value that will be compared.</param>
		public void AddClause(LogicalJoin logicalJoin, string fieldRefName, ComparisonOperator comparisonOperator, object value, CamlValueType valueType)
		{
			this.AddClause(logicalJoin, comparisonOperator, null, fieldRefName, valueType, value);
		}

		/// <summary>
		/// Adds a clause to the &lt;OrderBy&gt; element.
		/// </summary>
		/// <param name="fieldName">The Field's internal name.</param>
		/// <param name="orderByDirection">The direction to order the results.</param>
		public void AddOrderBy(string fieldName, OrderByDirection orderByDirection)
		{
			this.AddOrderBy(fieldName, null, orderByDirection);
		}

		/// <summary>
		/// Adds a clause to the &lt;OrderBy&gt; element.
		/// </summary>
		/// <param name="fieldId">The field id.</param>
		/// <param name="orderByDirection">The direction to order the results.</param>
		public void AddOrderBy(Guid fieldId, OrderByDirection orderByDirection)
		{
			this.AddOrderBy(null, fieldId, orderByDirection);
		}

		/// <summary>
		/// Adds a clause to the &lt;GroupBy&gt; element.
		/// </summary>
		/// <param name="fieldName">The Field's internal name.</param>		
		public void AddGroupBy(string fieldName)
		{
			this.AddGroupBy(fieldName, null);
		}

		/// <summary>
		/// Adds a clause to the &lt;GroupBy&gt; element.
		/// </summary>
		/// <param name="fieldId">The field id.</param>
		public void AddGroupBy(Guid fieldId)
		{
			this.AddGroupBy(null, fieldId);
		}

		/// <summary>
		/// Adds a ViewField to the &lt;ViewFields&gt; element.
		/// </summary>
		/// <param name="fieldName">The Field's internal name.</param>		
		public void AddViewField(string fieldName)
		{
			this.AddViewField(fieldName, null);
		}

		/// <summary>
		/// Adds a ViewField to the &lt;ViewFields&gt; element.
		/// </summary>
		/// <param name="fieldId">The field id.</param>
		public void AddViewField(Guid fieldId)
		{
			this.AddViewField(null, fieldId);
		}

		/// <summary>
		/// Adds the view fields.
		/// </summary>
		/// <param name="fieldNames">The field names.</param>
		public void AddViewFields(params string[] fieldNames)
		{
			foreach (string fieldName in fieldNames)
			{
				this.AddViewField(fieldName);
			}
		}

		/// <summary>
		/// Adds the view fields.
		/// </summary>
		/// <param name="fieldIds">The field ids.</param>
		public void AddViewFields(params Guid[] fieldIds)
		{
			foreach (Guid fieldId in fieldIds)
			{
				this.AddViewField(fieldId);
			}
		}

		/// <summary>
		/// Gets the CAML query as a string, including the top-level &lt;Query&gt; element.
		/// </summary>
		public override string ToString()
		{
			XElement queryElement = BuildQueryElement();
			return queryElement.ToString();
		}

		private void AddClause(CamlQueryClause queryClause)
		{
			this.QueryClauses.Add(queryClause);
		}

		private void AddClause(LogicalJoin logicalJoin, ComparisonOperator comparisonOperator, Guid? fieldRefId, string fieldRefName, CamlValueType valueType, object value)
		{
			CamlQueryClause camlQueryClause = new CamlQueryClause();
			camlQueryClause.LogicalJoin = logicalJoin;
			camlQueryClause.ComparisonOperator = comparisonOperator;
			camlQueryClause.FieldRefId = fieldRefId;
			camlQueryClause.FieldRefName = fieldRefName;
			camlQueryClause.ValueType = valueType;
			camlQueryClause.Value = value;

			this.AddClause(camlQueryClause);
		}

		private void AddOrderBy(OrderByClause orderByClause)
		{
			this.OrderByClauses.Add(orderByClause);
		}

		private void AddOrderBy(string fieldName, Guid? fieldId, OrderByDirection orderByDirection)
		{
			OrderByClause orderByClause = new OrderByClause();
			orderByClause.FieldRefName = fieldName;
			orderByClause.FieldRefId = fieldId;
			orderByClause.OrderByDirection = orderByDirection;

			this.AddOrderBy(orderByClause);
		}

		private void AddGroupBy(GroupByClause groupByClause)
		{
			this.GroupByClauses.Add(groupByClause);
		}

		private void AddGroupBy(string fieldName, Guid? fieldId)
		{
			GroupByClause groupByClause = new GroupByClause();
			groupByClause.FieldRefName = fieldName;
			groupByClause.FieldRefId = fieldId;

			this.AddGroupBy(groupByClause);
		}

		private void AddViewField(ViewField viewField)
		{
			this.ViewFields.Add(viewField);
		}

		private void AddViewField(string fieldName, Guid? fieldId)
		{
			ViewField viewField = new ViewField();
			viewField.FieldRefName = fieldName;
			viewField.FieldRefId = fieldId;

			this.AddViewField(viewField);
		}

		private XElement BuildQueryElement()
		{
			XElement queryElement = new XElement(CamlConstants.Query);
			XElement whereElement = BuildWhereElement();
			XElement orderByElement = BuildOrderByElement();
			XElement groupByElement = BuildGroupByElement();

			queryElement.Add(whereElement);
			queryElement.Add(orderByElement);
			queryElement.Add(groupByElement);

			return queryElement;
		}

		private XElement BuildWhereElement()
		{
			XElement whereElement = null;
			if (this.QueryClauses.Count > 0)
			{
				whereElement = new XElement(CamlConstants.Where);
				AddClauseToQuery(0, whereElement);
			}
			return whereElement;
		}

		private XElement BuildOrderByElement()
		{
			XElement orderByElement = null;
			if (this.OrderByClauses.Count > 0)
			{
				orderByElement = new XElement(CamlConstants.OrderBy);
				foreach (OrderByClause orderByClause in this.OrderByClauses)
				{
					XElement fieldRefElement = new XElement(CamlConstants.FieldRef);
					AddAttributeIfNotNull(CamlConstants.Name, orderByClause.FieldRefName, fieldRefElement);
					AddAttributeIfNotNull(CamlConstants.ID, orderByClause.FieldRefId, fieldRefElement);

					string isAscending = (orderByClause.OrderByDirection == OrderByDirection.Ascending) ? CamlConstants.TRUE : CamlConstants.FALSE;

					fieldRefElement.Add(new XAttribute(CamlConstants.Ascending, isAscending));

					orderByElement.Add(fieldRefElement);
				}
			}
			return orderByElement;
		}

		private XElement BuildGroupByElement()
		{
			XElement groupByElement = null;
			if (this.GroupByClauses.Count > 0)
			{
				groupByElement = new XElement(CamlConstants.GroupBy);
				foreach (GroupByClause groupByClause in this.GroupByClauses)
				{
					XElement fieldRefElement = new XElement(CamlConstants.FieldRef);
					AddAttributeIfNotNull(CamlConstants.Name, groupByClause.FieldRefName, fieldRefElement);
					AddAttributeIfNotNull(CamlConstants.ID, groupByClause.FieldRefId, fieldRefElement);

					groupByElement.Add(fieldRefElement);
				}
			}
			return groupByElement;
		}

		private XElement BuildViewFieldsElement()
		{
			XElement viewFieldElement = null;
			if (this.ViewFields.Count > 0)
			{
				viewFieldElement = new XElement(CamlConstants.ViewFields);
				foreach (ViewField viewField in this.ViewFields)
				{
					XElement fieldRefElement = new XElement(CamlConstants.FieldRef);
					AddAttributeIfNotNull(CamlConstants.Name, viewField.FieldRefName, fieldRefElement);
					AddAttributeIfNotNull(CamlConstants.ID, viewField.FieldRefId, fieldRefElement);

					viewFieldElement.Add(fieldRefElement);
				}
			}
			return viewFieldElement;
		}

		private string BuildSPQueryString(bool removeWhiteSpace)
		{
			SaveOptions saveOptions = removeWhiteSpace ? SaveOptions.DisableFormatting : SaveOptions.None;

			string spQueryString = null;

			XElement whereElement = BuildWhereElement();
			if (whereElement != null)
			{
				spQueryString = whereElement.ToString(saveOptions);
			}			

			XElement orderByElement = BuildOrderByElement();
			if (orderByElement != null)
			{
				if (!removeWhiteSpace)
				{
					spQueryString += (Environment.NewLine);
				}
				spQueryString += orderByElement.ToString(saveOptions);
			}

			XElement groupByElement = BuildGroupByElement();
			if (groupByElement != null)
			{
				if (!removeWhiteSpace)
				{
					spQueryString += (Environment.NewLine);
				}
				spQueryString += groupByElement.ToString(saveOptions);
			}

			return spQueryString;
		}

		private string BuildSPQueryViewFields(bool removeWhiteSpace)
		{
			SaveOptions saveOptions = removeWhiteSpace ? SaveOptions.DisableFormatting : SaveOptions.None;

			string viewFieldsString = null;
			XElement viewFieldsElement = this.BuildViewFieldsElement();
			if (viewFieldsElement != null)
			{
				var descendants = viewFieldsElement.DescendantNodes().OfType<XElement>();
				StringBuilder sb = new StringBuilder();
				foreach (XElement element in descendants)
				{
					sb.Append(element.ToString(saveOptions));
					if (!removeWhiteSpace)
					{
						sb.AppendLine();
					}
				}
				viewFieldsString = sb.ToString();
			}
			return viewFieldsString;
		}

		private void AddClauseToQuery(int index, XElement element)
		{
			if (index == this.QueryClauses.Count)
			{
				return;
			}

			CamlQueryClause currentClause = this.QueryClauses[index];

			XElement newElement = new XElement(currentClause.ComparisonOperator.ToString());

			XElement fieldRefElement = new XElement(CamlConstants.FieldRef);
			AddAttributeIfNotNull(CamlConstants.Name, currentClause.FieldRefName, fieldRefElement);
			AddAttributeIfNotNull(CamlConstants.ID, currentClause.FieldRefId, fieldRefElement);

			if (currentClause.ValueType == CamlValueType.Lookup)
			{
				fieldRefElement.Add(new XAttribute(CamlConstants.LookupId, CamlConstants.TRUE));
			}

			newElement.Add(fieldRefElement);

			if (currentClause.Value != null && 
				currentClause.ComparisonOperator != ComparisonOperator.IsNull && 
				currentClause.ComparisonOperator != ComparisonOperator.IsNotNull)
			{
				XElement valueElement = new XElement(CamlConstants.Value);
				AddAttributeIfNotNull(CamlConstants.Type, currentClause.ValueType, valueElement);
				valueElement.Add(currentClause.Value);

				newElement.Add(valueElement);
			}

			if (index < (this.QueryClauses.Count - 1))
			{
				CamlQueryClause nextClause = this.QueryClauses[index + 1];
				newElement = new XElement(nextClause.LogicalJoin.ToString(), newElement);
			}

			AddClauseToQuery(index + 1, newElement);
			element.Add(newElement);
		}

		private static void AddAttributeIfNotNull(string attributeName, object value, XElement element)
		{
			if (value != null)
			{
				element.Add(new XAttribute(attributeName, value));
			}
		}
	}
}
